Skip to main content

copp\copp\copp2\opt2/
copp2_socp.rs

1//! 2nd-order Convex-Objective Path Parameterization (COPP2) based on second-order cone programming (SOCP).
2//!
3//! # Method identity
4//! This module implements the **optimization backend** for COPP2 by transforming path-parameterization
5//! constraints/objectives into a Clarabel-compatible conic form and solving it with SOCP.
6//!
7//! # Discrete variables (local notation)
8//! On a path grid `s[0..=n]`:
9//! - `a[k]` denotes $\dot{s}_k^2$ (state variable, expected nonnegative in feasible solutions);
10//! - decision vector is organized as `x = [a[0..=n], x_others]`, where `x_others` are auxiliary variables introduced by objective terms (e.g. reciprocal/soc slack variables);
11//!
12//! # High-level pipeline
13//! 1. Validate interval and boundary consistency.
14//! 2. Estimate capacities and assemble standard TOPP2 constraints.
15//! 3. Add COPP2 objective-induced variables/cones ([`Time`](crate::prelude::CoppObjective::Time), [`ThermalEnergy`](crate::prelude::CoppObjective::ThermalEnergy), [`TotalVariationTorque`](crate::prelude::CoppObjective::TotalVariationTorque), [`Linear`](crate::prelude::CoppObjective::Linear)).
16//! 4. Build sparse matrices `A`, `P`, vector `q`, and solve by Clarabel.
17//! 5. Apply status acceptance policy ([`ClarabelOptions::is_allow`](crate::solver::copp2_socp::ClarabelOptions::is_allow)) and extract `a` when allowed.
18//!
19//! # API layering
20//! - [`copp2_socp`](crate::solver::copp2_socp::copp2_socp): strict/normal API, returns only accepted `a`.
21//! - [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert): expert API, always returns full Clarabel solution for diagnosis.
22//! - [`copp2_socp_expert_with_info`](crate::solver::copp2_socp::copp2_socp_expert_with_info): expert API plus Clarabel linear-solver
23//!   metadata for solver-side diagnostics.
24
25use crate::copp::clarabel_backend::{ConstraintsClarabel, ObjConsClarabel};
26use crate::copp::copp2::formulation::Copp2Problem;
27use crate::copp::copp2::opt2::ClarabelExpertInfor2nd;
28use crate::copp::copp2::opt2::clarabel_constraints::{
29    clarabel_standard_capacity_topp2, clarabel_standard_constraint_topp2,
30};
31use crate::copp::{ClarabelOptions, CoppObjective, clarabel_to_copp2_solution};
32use crate::diag::{
33    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
34    format_duration_human,
35};
36use crate::robot::robot_core::{Robot, RobotBasic, RobotTorque};
37use clarabel::algebra::CscMatrix;
38use clarabel::solver::SupportedConeT::{NonnegativeConeT, SecondOrderConeT};
39use clarabel::solver::{DefaultSolution, DefaultSolver, IPSolver, SupportedConeT};
40use core::f64;
41use itertools::{Itertools, izip};
42use nalgebra::{DMatrix, DVectorView};
43
44use crate::copp::copp2::stable::basic::a_to_b_topp2;
45
46/// Strict COPP2-SOCP API for production use.
47///
48/// # Purpose
49/// Use this entry when caller only needs a valid trajectory profile `a` and treats
50/// non-accepted solver statuses as hard failures.
51///
52/// # Contract
53/// - Internally calls [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert).
54/// - Returns `Ok(a)` **iff** `options.is_allow(solution.status)` is `true`.
55/// - Returns `Err(CoppError::ClarabelSolverStatus(...))` when status is not accepted.
56///
57/// # Returns
58/// Returns accepted profile `a` for production usage.
59///
60/// # Errors
61/// Returns [`CoppError`](crate::diag::CoppError) for model/solver failures and when solver status is not accepted.
62///
63/// # Notes
64/// For workflows requiring low-level diagnostics (`status`, iterate behavior, residual-related fields in
65/// Clarabel solution), prefer [`copp2_socp_expert`](crate::solver::copp2_socp::copp2_socp_expert).
66pub fn copp2_socp<'a, M: RobotTorque>(
67    problem: &Copp2Problem<'a, M>,
68    options: &ClarabelOptions,
69) -> Result<Vec<f64>, CoppError> {
70    let (a_profile, solution) = copp2_socp_expert(problem, options)?;
71    a_profile.ok_or_else(|| CoppError::ClarabelSolverStatus("copp2_socp".into(), solution.status))
72}
73
74/// Expert COPP2-SOCP API with full Clarabel solution exposure.
75///
76/// # Purpose
77/// This API is intended for advanced users who need both:
78/// - extracted high-level profile `Option<Vec<f64>>`, and
79/// - raw solver result [`DefaultSolution<f64>`](clarabel::solver::DefaultSolution) for post-analysis.
80///
81/// # Return contract
82/// - `Ok((Some(a), solution))`: status accepted by `options.is_allow(solution.status)`.
83/// - `Ok((None, solution))`: solve finished but status not accepted by policy.
84/// - `Err(...)`: true runtime failures only (input validation / model build / solver construction).
85///
86/// # Returns
87/// Returns tuple `(Option<Vec<f64>>, DefaultSolution<f64>)` for diagnostic workflows.
88///
89/// # Errors
90/// Returns [`CoppError`](crate::diag::CoppError) only for real failures (input, model build, or solver runtime).
91///
92/// # Contract
93/// - caller must handle `None` profile when status is not accepted;
94/// - acceptance policy is fully controlled by `options.is_allow`.
95///
96/// # Verbosity behavior
97/// Logging is layered by `options.verbosity()`:
98/// - [`Silent`](crate::diag::Verbosity::Silent): no algorithm logs;
99/// - [`Summary`](crate::diag::Verbosity::Summary): lifecycle milestones and elapsed time;
100/// - [`Debug`](crate::diag::Verbosity::Debug): assembly-level counters and stage summaries;
101/// - [`Trace`](crate::diag::Verbosity::Trace): fine-grained stage deltas and solver snapshot diagnostics.
102pub fn copp2_socp_expert<'a, M: RobotTorque>(
103    problem: &Copp2Problem<'a, M>,
104    options: &ClarabelOptions,
105) -> Result<(Option<Vec<f64>>, DefaultSolution<f64>), CoppError> {
106    let result = copp2_socp_expert_with_info(problem, options)?;
107    Ok((result.result, result.solution))
108}
109
110/// Expert COPP2-SOCP API with Clarabel solution and linear-solver diagnostics.
111///
112/// Use this variant when callers need more than
113/// [`DefaultSolution`](clarabel::solver::DefaultSolution), because Clarabel stores linear solver metadata on the
114/// solver `info` object rather than inside the returned solution.
115pub fn copp2_socp_expert_with_info<'a, M: RobotTorque>(
116    problem: &Copp2Problem<'a, M>,
117    options: &ClarabelOptions,
118) -> Result<ClarabelExpertInfor2nd, CoppError> {
119    match options.verbosity() {
120        Verbosity::Silent => copp2_socp_core(problem, (options, SilentVerboser)),
121        Verbosity::Summary => copp2_socp_core(problem, (options, SummaryVerboser::new())),
122        Verbosity::Debug => copp2_socp_core(problem, (options, DebugVerboser::new())),
123        Verbosity::Trace => copp2_socp_core(problem, (options, TraceVerboser::new())),
124    }
125}
126
127/// Core implementation for COPP2-SOCP expert flow.
128///
129/// # Internal contract
130/// `options_verboser` packs:
131/// - `options`: acceptance policy and Clarabel numerical settings;
132/// - `verboser`: concrete logger implementation chosen by external verbosity dispatch.
133///
134/// # Invariants
135/// - decision-variable layout always starts with contiguous `a[0..=n]`;
136/// - `q_object.len()` is treated as final `n_var` before solver build;
137/// - extracted `a` is produced only through [`clarabel_to_copp2_solution`](crate::solver::copp2_socp::clarabel_to_copp2_solution) when status is accepted.
138fn copp2_socp_core<'a, M: RobotTorque>(
139    problem: &Copp2Problem<'a, M>,
140    options_verboser: (&ClarabelOptions, impl Verboser),
141) -> Result<ClarabelExpertInfor2nd, CoppError> {
142    let (options, mut verboser) = options_verboser;
143    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
144    if verboser.is_enabled(Verbosity::Summary) {
145        verboser.record_start_time();
146        crate::verbosity_log!(
147            crate::diag::Verbosity::Summary,
148            "\ncopp2_socp started: {} <= idx_s <= {}, objectives = {}, s_len = {}.",
149            idx_s_start,
150            idx_s_final,
151            problem.objectives.len(),
152            problem.s_len()
153        );
154    }
155    if verboser.is_enabled(Verbosity::Trace) {
156        let settings = options.clarabel_settings();
157        crate::verbosity_log!(
158            crate::diag::Verbosity::Summary,
159            "copp2_socp: options snapshot -> allow(almost={}, max_iter={}, max_time={}, callback_term={}, insufficient_progress={}), tol_gap_rel={}, tol_feas={}, max_iter={}, verbose={}",
160            options.is_allow(clarabel::solver::SolverStatus::AlmostSolved),
161            options.is_allow(clarabel::solver::SolverStatus::MaxIterations),
162            options.is_allow(clarabel::solver::SolverStatus::MaxTime),
163            options.is_allow(clarabel::solver::SolverStatus::CallbackTerminated),
164            options.is_allow(clarabel::solver::SolverStatus::InsufficientProgress),
165            settings.tol_gap_rel,
166            settings.tol_feas,
167            settings.max_iter,
168            settings.verbose
169        );
170    }
171    // Check input validity
172    let n = idx_s_final - idx_s_start;
173    // Let x = [a[0], a[1], ..., a[n], x_others] \in R^{n+1+n_others}.
174    // Step 1. Deal with constraints
175    // Step 1.1 Compute the number of constraints
176    let (cap_val_std, cap_b_std, cap_cone_std) =
177        clarabel_standard_capacity_topp2(&problem.robot.constraints, problem.idx_s_interval);
178    let (cap_val_obj, cap_b_obj, cap_cone_obj, n_vars) =
179        clarabel_objective_capacity_copp2(n, problem.objectives, problem.robot);
180    if verboser.is_enabled(Verbosity::Debug) {
181        crate::verbosity_log!(
182            crate::diag::Verbosity::Summary,
183            "copp2_socp: capacity estimate std(val={cap_val_std}, b={cap_b_std}, cone={cap_cone_std}), obj(val={cap_val_obj}, b={cap_b_obj}, cone={cap_cone_obj}), n_vars={n_vars}."
184        );
185    }
186    // s=b-A*x \in cone, where A[row[i],col[i]]=val[i], A \in R^{m*(n+1)}, b \in R^m, s \in R^m
187    // -s=-b+A*x
188    let mut row = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
189    let mut col = Vec::<usize>::with_capacity(cap_val_std + cap_val_obj);
190    let mut val = Vec::<f64>::with_capacity(cap_val_std + cap_val_obj);
191    let mut b = Vec::<f64>::with_capacity(cap_b_std + cap_b_obj);
192    let mut cones = Vec::<SupportedConeT<f64>>::with_capacity(cap_cone_std + cap_cone_obj);
193    if verboser.is_enabled(Verbosity::Trace) {
194        crate::verbosity_log!(
195            crate::diag::Verbosity::Summary,
196            "copp2_socp: allocated capacities row/col/val/b/cones <= {}/{}/{}/{}/{}",
197            cap_val_std + cap_val_obj,
198            cap_val_std + cap_val_obj,
199            cap_val_std + cap_val_obj,
200            cap_b_std + cap_b_obj,
201            cap_cone_std + cap_cone_obj
202        );
203    }
204    // Step 1.2 set constraints
205    let row_before_std = row.len();
206    let col_before_std = col.len();
207    let val_before_std = val.len();
208    let b_before_std = b.len();
209    let cones_before_std = cones.len();
210    clarabel_standard_constraint_topp2(
211        &problem.as_topp2_problem(),
212        (&mut row, &mut col, &mut val, &mut b, &mut cones),
213        &verboser,
214    );
215    if verboser.is_enabled(Verbosity::Trace) {
216        crate::verbosity_log!(
217            crate::diag::Verbosity::Summary,
218            "copp2_socp: standard-constraints delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}",
219            row.len() - row_before_std,
220            col.len() - col_before_std,
221            val.len() - val_before_std,
222            b.len() - b_before_std,
223            cones.len() - cones_before_std
224        );
225    }
226    // Step 2. set objective
227    // Step 2.1. determine whether eta=1/sqrt(a) is needed.
228    let row_before_sqrt = row.len();
229    let col_before_sqrt = col.len();
230    let val_before_sqrt = val.len();
231    let b_before_sqrt = b.len();
232    let cones_before_sqrt = cones.len();
233    let n_var_old = clarabel_sqrt_a_copp2(
234        n,
235        problem.objectives,
236        (&mut row, &mut col, &mut val, &mut b, &mut cones),
237    );
238    if verboser.is_enabled(Verbosity::Trace) {
239        crate::verbosity_log!(
240            crate::diag::Verbosity::Summary,
241            "copp2_socp: sqrt-a stage delta row/col/val/b/cones = +{}/+{}/+{}/+{}/+{}, n_var_old={}",
242            row.len() - row_before_sqrt,
243            col.len() - col_before_sqrt,
244            val.len() - val_before_sqrt,
245            b.len() - b_before_sqrt,
246            cones.len() - cones_before_sqrt,
247            n_var_old
248        );
249    }
250    let mut q_object = Vec::<f64>::with_capacity(n_vars);
251    q_object.resize(n_var_old, 0.0);
252    // Step 2.2. add constraints and objective for each term in the objective.
253    let row_before_obj = row.len();
254    let col_before_obj = col.len();
255    let val_before_obj = val.len();
256    let b_before_obj = b.len();
257    let cones_before_obj = cones.len();
258    let q_before_obj = q_object.len();
259    clarable_objective_copp2(
260        problem,
261        (
262            &mut row,
263            &mut col,
264            &mut val,
265            &mut b,
266            &mut cones,
267            &mut q_object,
268        ),
269    )?;
270    if verboser.is_enabled(Verbosity::Trace) {
271        let (q_min, q_max) = q_object
272            .iter()
273            .fold((f64::INFINITY, f64::NEG_INFINITY), |(mn, mx), &v| {
274                (mn.min(v), mx.max(v))
275            });
276        crate::verbosity_log!(
277            crate::diag::Verbosity::Summary,
278            "copp2_socp: objective stage delta row/col/val/b/cones/q = +{}/+{}/+{}/+{}/+{}/+{}, q_range=[{}, {}]",
279            row.len() - row_before_obj,
280            col.len() - col_before_obj,
281            val.len() - val_before_obj,
282            b.len() - b_before_obj,
283            cones.len() - cones_before_obj,
284            q_object.len() - q_before_obj,
285            q_min,
286            q_max
287        );
288    }
289    if verboser.is_enabled(Verbosity::Debug) {
290        crate::verbosity_log!(
291            crate::diag::Verbosity::Summary,
292            "copp2_socp: after objective assembly row={}, col={}, val={}, b={}, cones={}, q={}",
293            row.len(),
294            col.len(),
295            val.len(),
296            b.len(),
297            cones.len(),
298            q_object.len()
299        );
300    }
301    if verboser.is_enabled(Verbosity::Summary) {
302        crate::verbosity_log!(
303            crate::diag::Verbosity::Summary,
304            "copp2_socp: ready to solve with row/col/val/b/cones = {}/{}/{}/{}/{} and n_var = {}.",
305            row.len(),
306            col.len(),
307            val.len(),
308            b.len(),
309            cones.len(),
310            q_object.len()
311        );
312    }
313    // Step 2.3 build the constraints
314    let n_var = q_object.len();
315    let a_csc = CscMatrix::new_from_triplets(b.len(), n_var, row, col, val);
316    let p_object = CscMatrix::<f64>::zeros((n_var, n_var));
317    if verboser.is_enabled(Verbosity::Trace) {
318        crate::verbosity_log!(
319            crate::diag::Verbosity::Summary,
320            "copp2_socp: matrix built with m={}, n={}, A.nnz={}, P.nnz={}",
321            b.len(),
322            n_var,
323            a_csc.nnz(),
324            p_object.nnz()
325        );
326    }
327    // Step 3. solve the SOCP problem
328    let settings = options.clarabel_settings().clone();
329    let mut solver = DefaultSolver::<f64>::new(&p_object, &q_object, &a_csc, &b, &cones, settings)
330        .map_err(|e| CoppError::ClarabelSolverError("copp2_socp".into(), e))?;
331    solver.solve();
332    let linsolver = solver.info.linsolver.clone();
333    let solution = solver.solution;
334    if verboser.is_enabled(Verbosity::Summary) {
335        crate::verbosity_log!(
336            crate::diag::Verbosity::Summary,
337            "copp2_socp: solve done, status = {:?}, elapsed = {}.",
338            solution.status,
339            format_duration_human(verboser.elapsed())
340        );
341    }
342    if verboser.is_enabled(Verbosity::Trace) {
343        let show = solution.x.len().min(3);
344        crate::verbosity_log!(
345            crate::diag::Verbosity::Summary,
346            "copp2_socp: solution x_len={}, head={:?}",
347            solution.x.len(),
348            &solution.x[0..show]
349        );
350    }
351    let a_profile = if options.is_allow(solution.status) {
352        Some(clarabel_to_copp2_solution(problem.s_len(), &solution))
353    } else {
354        None
355    };
356    if verboser.is_enabled(Verbosity::Trace) {
357        crate::verbosity_log!(
358            crate::diag::Verbosity::Summary,
359            "copp2_socp: allow(status)={}, extracted_profile={}",
360            options.is_allow(solution.status),
361            if a_profile.is_some() {
362                "Some(a)"
363            } else {
364                "None"
365            }
366        );
367    }
368    Ok(ClarabelExpertInfor2nd {
369        result: a_profile,
370        solution,
371        linsolver,
372    })
373}
374
375/// Determine the number of clarabel's capacity for the objective in COPP2.
376fn clarabel_objective_capacity_copp2<M: RobotBasic>(
377    n: usize,
378    objective: &[CoppObjective],
379    robot: &Robot<M>,
380) -> (usize, usize, usize, usize) {
381    let flag_need_eta = objective.iter().any(|obj| {
382        matches!(
383            obj,
384            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
385        )
386    });
387    // Step 1. sqrt(a[k]) >= eta[k] >= 0
388    // num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
389    let (mut capacity_val, mut capacity_b, mut capacity_cones, mut n_vars) = if flag_need_eta {
390        (4 * (n + 1), 4 * (n + 1), n + 2, 2 * (n + 1))
391    } else {
392        (0, 0, 0, n + 1)
393    };
394    // Step 2. objective function
395    let dim = robot.dim();
396    for obj in objective {
397        match obj {
398            CoppObjective::Time(_) => {
399                // num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
400                capacity_val += 6 * n;
401                capacity_b += 3 * n;
402                capacity_cones += n;
403                n_vars += n;
404            }
405            CoppObjective::ThermalEnergy(_, _) => {
406                // num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
407                capacity_val += (6 + 2 * dim) * n;
408                capacity_b += (dim + 2) * n;
409                capacity_cones += n;
410                n_vars += n;
411            }
412            CoppObjective::TotalVariationTorque(_, _) => {
413                // num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
414                capacity_val += 8 * dim * n;
415                capacity_b += 2 * dim * n;
416                capacity_cones += 1;
417                n_vars += dim * n;
418            }
419            _ => {}
420        }
421    }
422    (capacity_val, capacity_b, capacity_cones, n_vars)
423}
424
425/// Add the constraints for sqrt(a) >= eta in COPP2 optimization.
426/// x = [a[0], a[1], ..., a[n], eta[0], eta[1], ..., eta[n], ...] \in R^{2*(n+1)+...}.
427/// sqrt(a[k]) >= eta[k] >= 0
428/// num_val <= 4*(n+1), num_b <= 4*(n+1), num_cones <= n+2
429/// Return the len of the new x: n+1 or 2*(n+1)
430fn clarabel_sqrt_a_copp2(
431    n: usize,
432    objective: &[CoppObjective],
433    constraints: ConstraintsClarabel,
434) -> usize {
435    let (row, col, val, b, cones) = constraints;
436    for obj in objective {
437        match obj {
438            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _) => {
439                // eta >= 0
440                // A*x-b = -s = -1*eta[k] <= 0
441                row.extend(b.len()..b.len() + n + 1);
442                col.extend((n + 1)..(2 * (n + 1)));
443                val.resize(val.len() + n + 1, -1.0);
444                b.resize(b.len() + n + 1, 0.0);
445                cones.push(NonnegativeConeT(n + 1));
446                // sqrt(a) >= eta
447                // eta^2 <= a
448                // eta^2 + (a - 0.25)^2 <= (a + 0.25)^2
449                // -A*x+b = s = [a+0.25, a-0.25, eta] \in SOC
450                row.extend(b.len()..b.len() + 3 * (n + 1));
451                val.resize(val.len() + 3 * (n + 1), -1.0);
452                cones.resize(cones.len() + n + 1, SecondOrderConeT(3));
453                for k in 0..=n {
454                    col.extend([k, k, k + n + 1]);
455                    b.extend([0.25, -0.25, 0.0]);
456                }
457                return 2 * (n + 1);
458            }
459            _ => {}
460        }
461    }
462    n + 1
463}
464
465/// Add the constraints and objective for Time in COPP2 optimization.
466/// num_val <= 6*n, num_b <= 3*n, num_cones <= n, n_var <= n
467fn clarabel_objective_time_copp2(
468    s: &[f64],
469    weight: f64,
470    objective_constraints: ObjConsClarabel,
471) -> bool {
472    if weight < 0.0 {
473        return false;
474    }
475    let (row, col, val, b, cones, q_object) = objective_constraints;
476    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1])
477    // Let: 1 / (eta[k] + eta[k+1]) <= 4 * t[k]
478    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
479    let weight = 8.0 * weight;
480    let n_var_old = q_object.len();
481    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
482    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
483    // t[k] * (eta[k] + eta[k+1]) >= 1
484    // (eta[k] + eta[k+1] + t[k])^2 >= (eta[k] + eta[k+1] - t[k])^2 + 1
485    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], 1] \in SOC
486    let len = s.len();
487    for k in 0..(len - 1) {
488        // eta[k] + eta[k+1] + t[k]
489        row.resize(row.len() + 3, b.len());
490        col.extend([len + k, len + k + 1, n_var_old + k]);
491        val.extend([-1.0, -1.0, -1.0]);
492        b.push(0.0);
493        // eta[k] + eta[k+1] - t[k]
494        row.resize(row.len() + 3, b.len());
495        col.extend([len + k, len + k + 1, n_var_old + k]);
496        val.extend([-1.0, -1.0, 1.0]);
497        b.push(0.0);
498        // 1
499        b.push(1.0);
500    }
501    cones.resize(cones.len() + len - 1, SecondOrderConeT(3));
502    true
503}
504
505/// Add the constraints and objective for ThermalEnergy in COPP2 optimization.
506/// num_val <= (6+2*dim)*n, num_b <= (dim+2)*n, num_cones <= n, n_var <= n
507fn clarabel_objective_thermal_energy_copp2(
508    s: &[f64],
509    weight: f64,
510    normalize: &[f64],
511    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
512    objective_constraints: ObjConsClarabel,
513) -> bool {
514    if weight < 0.0 {
515        return false;
516    }
517    let (row, col, val, b, cones, q_object) = objective_constraints;
518    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
519    // minimize: 2 * weight * \sum (s[k+1]-s[k]) / (eta[k] + eta[k+1]) * (tau[i][k] * normalize[i]) ^ 2
520    // Let: \sum_i (tau[i][k] * normalize[i]) ^ 2 / (eta[k] + eta[k+1]) <= 4 * t[k]
521    let len = s.len();
522    let mut coeff_a_curr = coeffs_torque.0.clone();
523    let mut coeff_a_next = coeffs_torque.1.clone();
524    let mut coeff_g = coeffs_torque.2.clone();
525    // objective: minimize 8 * weight * \sum (s[k+1]-s[k]) * t[k]
526    let weight = 8.0 * weight;
527    let n_var_old = q_object.len();
528    // objective: minimize weight * \sum (s[k+1]-s[k]) * t[k]
529    q_object.extend(s.windows(2).map(|s_pair| weight * (s_pair[1] - s_pair[0])));
530    // \sum_i (tau[i][k] * normalize[i]) ^ 2 <= 4 * t[k] * (eta[k] + eta[k+1])
531    let dim = coeff_a_curr.nrows();
532    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
533    if normalize.len() != dim {
534        return false;
535    }
536    let normalize = DVectorView::from_slice(normalize, dim);
537    for mut col in coeff_a_curr.column_iter_mut() {
538        col.component_mul_assign(&normalize);
539    }
540    for mut col in coeff_a_next.column_iter_mut() {
541        col.component_mul_assign(&normalize);
542    }
543    for mut col in coeff_g.column_iter_mut() {
544        col.component_mul_assign(&normalize);
545    }
546    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
547
548    // (eta[k] + eta[k+1] - t[k])^2 + \sum_i (tau[i][k] * normalize[i]) ^ 2 <= (eta[k] + eta[k+1] + t[k])^2
549    // -A*x+b = s = [eta[k] + eta[k+1] + t[k], eta[k] + eta[k+1] - t[k], tau[0][k] * normalize[0], tau[1][k] * normalize[1], ...] \in SOC
550    for (k, (col_a_curr, col_a_next, col_g)) in izip!(
551        coeff_a_curr.column_iter(),
552        coeff_a_next.column_iter(),
553        coeff_g.column_iter()
554    )
555    .enumerate()
556    {
557        // eta[k] + eta[k+1] + t[k]
558        row.resize(row.len() + 3, b.len());
559        col.extend([len + k, len + k + 1, n_var_old + k]);
560        val.extend([-1.0, -1.0, -1.0]);
561        b.push(0.0);
562        // eta[k] + eta[k+1] - t[k]
563        row.resize(row.len() + 3, b.len());
564        col.extend([len + k, len + k + 1, n_var_old + k]);
565        val.extend([-1.0, -1.0, 1.0]);
566        b.push(0.0);
567        // tau[i][k] * normalize[i] = col_a_curr[i] * x[k] + col_a_next[i] * x[k+1] + col_g[i]
568        for (&v_a_curr, &v_a_next, &v_g) in
569            izip!(col_a_curr.iter(), col_a_next.iter(), col_g.iter())
570        {
571            row.resize(row.len() + 2, b.len());
572            col.extend([k, k + 1]);
573            val.extend([v_a_curr, v_a_next]);
574            b.push(v_g);
575        }
576    }
577    cones.resize(cones.len() + len - 1, SecondOrderConeT(dim + 2));
578    true
579}
580
581/// Add the constraints and objective for TotalVariationTorque in COPP2 optimization.
582/// num_val <= 8*dim*n, num_b <= 2*dim*n, num_cones <= 1, n_var <= dim*n
583fn clarabel_objective_tv_torque_copp2(
584    weight: f64,
585    normalize: &[f64],
586    coeffs_torque: &(DMatrix<f64>, DMatrix<f64>, DMatrix<f64>),
587    objective_constraints: ObjConsClarabel,
588) -> bool {
589    if weight < 0.0 {
590        return false;
591    }
592    let (row, col, val, b, cones, q_object) = objective_constraints;
593    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
594    // Let: |tau[i][k+1]-tau[i][k]| * normalize[i] <= t[i][k]
595    let mut coeff_a_curr = coeffs_torque.0.clone();
596    let mut coeff_a_next = coeffs_torque.1.clone();
597    let mut coeff_g = coeffs_torque.2.clone();
598    let dim = coeff_a_curr.nrows();
599    if normalize.len() != dim {
600        return false;
601    }
602    // tau[i][k] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
603    let normalize = DVectorView::from_slice(normalize, dim);
604    for mut col in coeff_a_curr.column_iter_mut() {
605        col.component_mul_assign(&normalize);
606    }
607    for mut col in coeff_a_next.column_iter_mut() {
608        col.component_mul_assign(&normalize);
609    }
610    for mut col in coeff_g.column_iter_mut() {
611        col.component_mul_assign(&normalize);
612    }
613    // Now: tau[i][k] * normalize[i] = coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k]
614    // (tau[i][k+1]-tau[i][k]) * normalize[i] = (coeff_a_curr[i][k+1] * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + coeff_g[i][k+1]) - (coeff_a_curr[i][k] * a[k] + coeff_a_next[i][k] * a[k+1] + coeff_g[i][k])
615    // = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k])
616
617    let n_b_old = b.len();
618    // A*x-b = -s = -coeff_a_curr[i][k] * a[k] + (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] + coeff_a_next[i][k+1] * a[k+2] + (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
619    // A*x-b = -s = coeff_a_curr[i][k] * a[k] - (coeff_a_curr[i][k+1] - coeff_a_next[i][k]) * a[k+1] - coeff_a_next[i][k+1] * a[k+2] - (coeff_g[i][k+1] - coeff_g[i][k]) - t[i][k] <= 0
620    let mut buffer0 = vec![0.0; dim];
621    let mut buffer1 = vec![0.0; dim];
622    for (k, ((col_a_curr, col_b_curr, col_g_curr), (col_a_next, col_b_next, col_g_next))) in izip!(
623        coeff_a_curr.column_iter(),
624        coeff_a_next.column_iter(),
625        coeff_g.column_iter()
626    )
627    .tuple_windows()
628    .enumerate()
629    {
630        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + (col_a_next[i] - col_b_curr[i]) * a[k+1] + col_b_next[i] * a[k+2] + (col_g_next[i] - col_g_curr[i])
631        buffer0.clear();
632        buffer1.clear();
633        buffer0.extend(
634            col_a_next
635                .iter()
636                .zip(col_b_curr.iter())
637                .map(|(&v_a_next, &v_b_curr)| v_b_curr - v_a_next),
638        );
639        buffer1.extend(
640            col_g_curr
641                .iter()
642                .zip(col_g_next.iter())
643                .map(|(&v_g_curr, &v_g_next)| v_g_curr - v_g_next),
644        );
645        // dtau[i] * normalize[i] = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i]
646
647        let n_var_old = q_object.len();
648        for (i, (&v0, &v1, &v_a_curr, &v_b_next)) in izip!(
649            buffer0.iter(),
650            buffer1.iter(),
651            col_a_curr.iter(),
652            col_b_next.iter()
653        )
654        .enumerate()
655        {
656            // -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] <= t[i][k]
657            // A*x-b = -s = -col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2] + buffer1[i] - t[i][k] <= 0
658            row.resize(row.len() + 4, b.len());
659            col.extend([k, k + 1, k + 2, n_var_old + i]);
660            val.extend([-v_a_curr, v0, v_b_next, -1.0]);
661            b.push(v1);
662
663            // -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) <= t[i][k]
664            // A*x-b = -s = -(-col_a_curr[i] * a[k] + buffer0[i] * a[k+1] + col_b_next[i] * a[k+2]) - t[i][k] <= 0
665            row.resize(row.len() + 4, b.len());
666            col.extend([k, k + 1, k + 2, n_var_old + i]);
667            val.extend([v_a_curr, -v0, -v_b_next, -1.0]);
668            b.push(-v1);
669        }
670
671        // objective: minimize weight * \sum t[i][k]
672        q_object.resize(q_object.len() + dim, weight);
673    }
674    cones.push(NonnegativeConeT(b.len() - n_b_old));
675    true
676}
677
678/// Add the constraints and objective for Linear in COPP2 optimization.
679fn clarabel_objective_linear_copp2(
680    s: &[f64],
681    weight: f64,
682    alpha: &[f64],
683    beta: &[f64],
684    q_object: &mut [f64],
685) -> bool {
686    if alpha.len() != s.len() || beta.len() != s.len() - 1 {
687        return false;
688    }
689    // objective: minimize weight * \sum (alpha[k]*a[k] + beta[k]*b[k])
690    // weight * \sum alpha[k]*a[k] + 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
691    for (va, q) in alpha.iter().zip(q_object.iter_mut()) {
692        // weight * \sum alpha[k]*a[k]
693        *q += weight * va;
694    }
695    for (s_pair, vb, q_curr) in izip!(s.windows(2), beta.iter(), q_object.iter_mut()) {
696        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
697        *q_curr -= 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
698    }
699    for (s_pair, vb, q_next) in izip!(s.windows(2), beta.iter(), q_object.iter_mut().skip(1)) {
700        // weight * \sum 0.5 * beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
701        *q_next += 0.5 * weight * vb / (s_pair[1] - s_pair[0]);
702    }
703    true
704}
705
706fn clarable_objective_copp2<M: RobotTorque>(
707    problem: &Copp2Problem<M>,
708    objective_constraints: ObjConsClarabel,
709) -> Result<(), CoppError> {
710    let (row, col, val, b, cones, q_object) = objective_constraints;
711    let s = problem
712        .robot
713        .constraints
714        .s_vec(problem.idx_s_interval.0, problem.idx_s_interval.1 + 1)?;
715    let coeffs_torque = if problem.objectives.iter().any(|obj| {
716        matches!(
717            obj,
718            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
719        )
720    }) {
721        // shape: (dim, n) since there are n+1 a and n b.
722        problem.robot.torque2_coeff_a(
723            problem.idx_s_interval.0,
724            problem.idx_s_interval.1 - problem.idx_s_interval.0,
725        )?
726    } else {
727        (
728            DMatrix::<f64>::zeros(0, 0),
729            DMatrix::<f64>::zeros(0, 0),
730            DMatrix::<f64>::zeros(0, 0),
731        )
732    };
733    for obj in problem.objectives {
734        match obj {
735            CoppObjective::Time(weight) => {
736                if !clarabel_objective_time_copp2(&s, *weight, (row, col, val, b, cones, q_object))
737                {
738                    return Err(CoppError::InvalidInput(
739                        "copp2_socp".into(),
740                        "Invalid Time objective.".into(),
741                    ));
742                }
743            }
744            CoppObjective::ThermalEnergy(weight, normalize) => {
745                if !clarabel_objective_thermal_energy_copp2(
746                    &s,
747                    *weight,
748                    normalize,
749                    &coeffs_torque,
750                    (row, col, val, b, cones, q_object),
751                ) {
752                    return Err(CoppError::InvalidInput(
753                        "copp2_socp".into(),
754                        "Invalid ThermalEnergy objective.".into(),
755                    ));
756                }
757            }
758            CoppObjective::TotalVariationTorque(weight, normalize) => {
759                if !clarabel_objective_tv_torque_copp2(
760                    *weight,
761                    normalize,
762                    &coeffs_torque,
763                    (row, col, val, b, cones, q_object),
764                ) {
765                    return Err(CoppError::InvalidInput(
766                        "copp2_socp".into(),
767                        "Invalid TotalVariationTorque objective.".into(),
768                    ));
769                }
770            }
771            CoppObjective::Linear(weight, alpha, beta) => {
772                if !clarabel_objective_linear_copp2(&s, *weight, alpha, beta, q_object) {
773                    return Err(CoppError::InvalidInput(
774                        "copp2_socp".into(),
775                        "Invalid Linear objective.".into(),
776                    ));
777                }
778            }
779        }
780    }
781    Ok(())
782}
783
784/// Compute the objective value for COPP2 optimization.
785pub(crate) fn objective_value_copp2_opt<M: RobotTorque>(
786    robot: &Robot<M>,
787    start_idx_s: usize,
788    objective: &[CoppObjective],
789    a_profile: &[f64],
790) -> (f64, Vec<f64>) {
791    let Ok(s) = robot
792        .constraints
793        .s_vec(start_idx_s, start_idx_s + a_profile.len())
794    else {
795        return (f64::INFINITY, vec![0.0; objective.len()]);
796    };
797    if a_profile.len() != s.len() {
798        return (f64::INFINITY, vec![0.0; objective.len()]);
799    }
800    let Ok(b_profile) = a_to_b_topp2(&s, a_profile) else {
801        return (f64::INFINITY, vec![0.0; objective.len()]);
802    };
803    let torque = if objective.iter().any(|obj| {
804        matches!(
805            obj,
806            CoppObjective::ThermalEnergy(_, _) | CoppObjective::TotalVariationTorque(_, _)
807        )
808    }) {
809        let torque_result =
810            robot.get_torque_with_ab(&a_profile[0..a_profile.len() - 1], &b_profile, start_idx_s);
811        match torque_result {
812            Ok(torque) => torque,
813            _ => return (f64::INFINITY, vec![0.0; objective.len()]),
814        }
815    } else {
816        DMatrix::<f64>::zeros(0, 0)
817    };
818    let a_sqrt = if objective.iter().any(|obj| {
819        matches!(
820            obj,
821            CoppObjective::Time(_) | CoppObjective::ThermalEnergy(_, _)
822        )
823    }) {
824        a_profile.iter().map(|a| a.sqrt()).collect()
825    } else {
826        Vec::new()
827    };
828    let mut obj_val = Vec::with_capacity(objective.len());
829    let mut obj_val_total = 0.0;
830    for obj in objective {
831        match obj {
832            CoppObjective::Time(weight) => {
833                let obj_here = objective_value_time_copp2(&s, &a_sqrt);
834                obj_val.push(obj_here);
835                obj_val_total += weight * obj_here;
836            }
837            CoppObjective::ThermalEnergy(weight, normalize) => {
838                let obj_here =
839                    objective_value_thermal_energy_copp2(&s, &a_sqrt, &torque, normalize);
840                obj_val.push(obj_here);
841                obj_val_total += weight * obj_here;
842            }
843            CoppObjective::TotalVariationTorque(weight, normalize) => {
844                let obj_here = objective_value_tv_torque_copp2(&torque, normalize);
845                obj_val.push(obj_here);
846                obj_val_total += weight * obj_here;
847            }
848            CoppObjective::Linear(weight, alpha, beta) => {
849                let obj_here = objective_value_linear_copp2(&s, a_profile, alpha, beta);
850                obj_val.push(obj_here);
851                obj_val_total += weight * obj_here;
852            }
853        }
854    }
855    (obj_val_total, obj_val)
856}
857
858/// Compute the time value in COPP2 optimization.
859/// Input: s, a_sqrt = sqrt(a)
860#[inline(always)]
861fn objective_value_time_copp2(s: &[f64], a_sqrt: &[f64]) -> f64 {
862    // objective: minimize 2 * weight * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1]))
863    let mut objective = 0.0;
864    for (s_pair, a_sqrt_pair) in s.windows(2).zip(a_sqrt.windows(2)) {
865        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]);
866    }
867    2.0 * objective
868}
869
870/// Compute the thermal energy value in COPP2 optimization.
871#[inline(always)]
872fn objective_value_thermal_energy_copp2(
873    s: &[f64],
874    a_sqrt: &[f64],
875    torque: &DMatrix<f64>,
876    normalize: &[f64],
877) -> f64 {
878    // minimize: 2 * \sum (s[k+1]-s[k]) / (sqrt(a[k]) + sqrt(a[k+1])) * (tau[i][k] * normalize[i]) ^ 2
879    let mut objective = 0.0;
880    for (s_pair, a_sqrt_pair, torque_col) in
881        izip!(s.windows(2), a_sqrt.windows(2), torque.column_iter())
882    {
883        let mut sum = 0.0;
884        for (torque, normal) in torque_col.iter().zip(normalize.iter()) {
885            sum += (torque * normal).powi(2);
886        }
887        objective += (s_pair[1] - s_pair[0]) / (a_sqrt_pair[0] + a_sqrt_pair[1]) * sum;
888    }
889    2.0 * objective
890}
891
892/// Compute the total variation of torque value in COPP2 optimization.
893#[inline(always)]
894fn objective_value_tv_torque_copp2(torque: &DMatrix<f64>, normalize: &[f64]) -> f64 {
895    // minimize: weight * \sum |tau[i][k+1]-tau[i][k]| * normalize[i]
896    let mut objective = 0.0;
897    for (torque_col_curr, torque_col_next) in torque.column_iter().tuple_windows() {
898        for (torque_prev, torque_next, normal) in izip!(
899            torque_col_curr.iter(),
900            torque_col_next.iter(),
901            normalize.iter()
902        ) {
903            objective += (torque_next - torque_prev).abs() * normal;
904        }
905    }
906    objective
907}
908
909/// Compute the objective value for Linear in COPP2 optimization.
910#[inline(always)]
911fn objective_value_linear_copp2(s: &[f64], a_profile: &[f64], alpha: &[f64], beta: &[f64]) -> f64 {
912    // objective: minimize \sum (alpha[k]*a[k] + beta[k]*b[k])
913    // \sum alpha[k]*a[k] + 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
914    let mut objective = 0.0;
915    for (a_curr, alpha_curr) in a_profile.iter().zip(alpha.iter()) {
916        // alpha[k]*a[k]
917        objective += a_curr * alpha_curr;
918    }
919    for (a_pair, s_pair, beta_curr) in izip!(a_profile.windows(2), s.windows(2), beta.iter()) {
920        // 0.5*beta[k]*(a[k+1]-a[k])/(s[k+1]-s[k])
921        objective += 0.5 * beta_curr * (a_pair[1] - a_pair[0]) / (s_pair[1] - s_pair[0]);
922    }
923    objective
924}
925
926#[cfg(test)]
927mod tests {
928    use super::*;
929    use crate::copp::copp2::stable::basic::{
930        Copp2ProblemBuilder, Topp2ProblemBuilder, s_to_t_topp2,
931    };
932    use crate::copp::copp2::stable::reach_set2::{ReachSet2Options, ReachSet2OptionsBuilder};
933    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
934    use crate::copp::{ClarabelOptions, ClarabelOptionsBuilder};
935    use crate::path::{
936        Path, SplineConfig, add_symmetric_axial_limits_for_test, lissajous_path_for_test,
937    };
938    use crate::robot::demo::Plannar2LinkEnd;
939    use crate::robot::robot_core::Robot;
940    use core::panic;
941    use nalgebra::DMatrix;
942    use std::time::Instant;
943    use std::vec;
944
945    #[test]
946    fn test_copp2_socp_only_time() -> Result<(), CoppError> {
947        run_test_copp2_socp_only_time_repeated(1, false)
948    }
949
950    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
951    /// Average 100 experiments: tc_ra = 0.2005 ms, tc_lp = 29.7361 ms, tc_qp = 166.5122 ms, tf_ra = 4.766745, tf_lp = 4.766745, tf_qp = 4.766735
952    #[test]
953    #[ignore = "slow"]
954    fn test_copp2_socp_only_time_robust() -> Result<(), CoppError> {
955        run_test_copp2_socp_only_time_repeated(100, true)?;
956        Ok(())
957    }
958
959    #[test]
960    fn test_copp2_socp() -> Result<(), CoppError> {
961        let options_socp = ClarabelOptionsBuilder::new()
962            .allow_almost_solved(true)
963            .build()?;
964        run_test_copp2_socp_once(&options_socp)
965    }
966
967    #[test]
968    #[ignore = "bindings"]
969    fn test_copp2_socp_bindings_parity() -> Result<(), CoppError> {
970        let dim = 3;
971        let num_waypoints = 8;
972        let n: usize = 81;
973        let pi = std::f64::consts::PI;
974
975        let waypoints = DMatrix::<f64>::from_fn(dim, num_waypoints, |axis, j| {
976            let s = j as f64 / (num_waypoints - 1) as f64;
977            match axis {
978                0 => 0.20 * (2.0 * pi * s).sin(),
979                1 => 0.15 * (1.5 * pi * s).cos(),
980                2 => 0.10 * s * (1.0 - s),
981                _ => unreachable!("dimension is fixed to 3"),
982            }
983        });
984        let path = Path::from_waypoints(&waypoints, SplineConfig::default())?;
985        let s = DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64);
986
987        let mut robot = Robot::with_capacity(dim, n);
988        robot
989            .with_s(&s.as_view())?
990            .with_q_from_path_2nd(&path, 0, n)?;
991        add_symmetric_axial_limits_for_test(&mut robot, 10.0, 50.0, None)?;
992        let torque_max = vec![1.0e6; dim];
993        let torque_min = vec![-1.0e6; dim];
994        robot.with_axial_torque((torque_max.as_slice(), n), (torque_min.as_slice(), n), 0)?;
995
996        let normalize = vec![1.0; dim];
997        let options = ClarabelOptionsBuilder::new()
998            .allow_almost_solved(true)
999            .build()?;
1000
1001        let objectives_thermal = [
1002            CoppObjective::Time(1.0),
1003            CoppObjective::ThermalEnergy(1.0, &normalize),
1004        ];
1005        let problem_thermal =
1006            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives_thermal)
1007                .build()?;
1008        let a_thermal = copp2_socp(&problem_thermal, &options)?;
1009        let (t_final_thermal, _) = s_to_t_topp2(s.as_slice(), &a_thermal, 0.0)?;
1010
1011        let objectives_tv = [
1012            CoppObjective::Time(1.0),
1013            CoppObjective::TotalVariationTorque(1.0, &normalize),
1014        ];
1015        let problem_tv =
1016            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &objectives_tv).build()?;
1017        let a_tv = copp2_socp(&problem_tv, &options)?;
1018
1019        crate::verbosity_log!(
1020            crate::diag::Verbosity::Summary,
1021            "COPP2-SOCP Rust bindings parity test: t_final_thermal={:.17}, thermal.len={}, tv.len={}",
1022            t_final_thermal,
1023            a_thermal.len(),
1024            a_tv.len()
1025        );
1026
1027        Ok(())
1028    }
1029
1030    /// Conditions: release, --include-ignored, CPU = Intel(R) Core(TM) Ultra 9 285K.
1031    /// Average 100 experiments:
1032    //  Case 0: tc=161.691ms, obj=[4.824561313613876, 2576.8873693126284, 71.69242263342399, -1.048938713665848e-14]
1033    //  Case 1: tc=200.959ms, obj=[4.830121022357044, 2576.928260526994, 66.14588916597324, -7.651101974204267e-15]
1034    //  Case 2: tc=218.223ms, obj=[4.830230167437296, 2576.9529624501106, 66.12305109242398, -1.09470765785602e-14]
1035    //  Case 3: tc=93.588ms, obj=[361.45110144118144, 194870.1172166501, 48.05756488144189, 9.743247875171334e-16]
1036    //  Case 4: tc=164.505ms, obj=[4.824561266838617, 2576.887346690997, 71.69241993761281, 2.7478852526741092e-14]
1037    #[test]
1038    #[ignore = "slow"]
1039    fn test_copp2_socp_robust() -> Result<(), CoppError> {
1040        run_test_copp2_socp_repeated(100, true)?;
1041        Ok(())
1042    }
1043
1044    fn run_test_copp2_socp_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
1045        let options_socp = ClarabelOptionsBuilder::new()
1046            .allow_almost_solved(true)
1047            .build()?;
1048
1049        let mut tc_sum_case0 = 0.0;
1050        let mut tc_sum_case1 = 0.0;
1051        let mut tc_sum_case2 = 0.0;
1052        let mut tc_sum_case3 = 0.0;
1053        let mut tc_sum_case4 = 0.0;
1054        let mut obj_sum_case0 = vec![0.0; 4];
1055        let mut obj_sum_case1 = vec![0.0; 4];
1056        let mut obj_sum_case2 = vec![0.0; 4];
1057        let mut obj_sum_case3 = vec![0.0; 4];
1058        let mut obj_sum_case4 = vec![0.0; 4];
1059
1060        for i_exp in 0..n_exp {
1061            let n: usize = 1000;
1062            let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1063            let dim = robot.dim();
1064
1065            let mut rng = rand::rng();
1066            let (s, path, omega, phi) =
1067                lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1068            robot
1069                .with_s(&s.as_view())?
1070                .with_q_from_path_2nd(&path, 0, n)?;
1071            add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1072
1073            // Test different objectives in COPP2 optimization
1074            let objectives_test = [
1075                CoppObjective::Time(1.0),
1076                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1077                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1078                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1079            ];
1080            let a_feasible = vec![0.0; n];
1081
1082            // Case 0: Time only
1083            let mut copp2_problem = Copp2ProblemBuilder::new(
1084                &robot,
1085                (0, n - 1),
1086                (0.0, 0.0),
1087                &[CoppObjective::Time(1.0)],
1088            )
1089            .build()?;
1090            let start = Instant::now();
1091            let mut a_case0 = copp2_socp(&copp2_problem, &options_socp)?;
1092            let tc_copp2_case0 = start.elapsed().as_secs_f64() * 1E3;
1093            robot
1094                .constraints
1095                .project_to_feasible_topp2(&mut a_case0, &a_feasible, 0)?;
1096            let (_, obj_case0) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case0);
1097
1098            // Case 1: Time and ThermalEnergy
1099            let obj_case1 = [
1100                CoppObjective::Time(1.0),
1101                CoppObjective::ThermalEnergy(1.0, &vec![1.0; dim]),
1102            ];
1103            copp2_problem.objectives = &obj_case1;
1104            let start = Instant::now();
1105            let mut a_case1 = copp2_socp(&copp2_problem, &options_socp)?;
1106            let tc_copp2_case1 = start.elapsed().as_secs_f64() * 1E3;
1107            robot
1108                .constraints
1109                .project_to_feasible_topp2(&mut a_case1, &a_feasible, 0)?;
1110            let (_, obj_case1) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case1);
1111            if obj_case1[0] < obj_case0[0] - 1E-3 || obj_case1[1] - 1E-3 > obj_case0[1] {
1112                let (tf_case0, _) = s_to_t_topp2(s.as_slice(), &a_case0, 0.0)?;
1113                let (tf_case1, _) = s_to_t_topp2(s.as_slice(), &a_case1, 0.0)?;
1114                crate::verbosity_log!(
1115                    crate::diag::Verbosity::Summary,
1116                    "omega = {omega:?}\nphi = {phi:?}"
1117                );
1118                crate::verbosity_log!(
1119                    crate::diag::Verbosity::Summary,
1120                    "Case 0: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1121                    obj_case0[0],
1122                    obj_case0[1],
1123                    tf_case0
1124                );
1125                crate::verbosity_log!(
1126                    crate::diag::Verbosity::Summary,
1127                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, tf = {:.6}",
1128                    obj_case1[0],
1129                    obj_case1[1],
1130                    tf_case1
1131                );
1132                crate::verbosity_log!(
1133                    crate::diag::Verbosity::Summary,
1134                    "Interesting... Cases 0 and 1"
1135                );
1136            }
1137
1138            // Case 2: Time and More ThermalEnergy
1139            let obj_case2 = [
1140                CoppObjective::Time(1.0),
1141                CoppObjective::ThermalEnergy(10.0, &vec![1.0; dim]),
1142            ];
1143            copp2_problem.objectives = &obj_case2;
1144            let start = Instant::now();
1145            let mut a_case2 = copp2_socp(&copp2_problem, &options_socp)?;
1146            let tc_copp2_case2 = start.elapsed().as_secs_f64() * 1E3;
1147            robot
1148                .constraints
1149                .project_to_feasible_topp2(&mut a_case2, &a_feasible, 0)?;
1150            let (_, obj_case2) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case2);
1151            if obj_case2[0] < obj_case1[0] - 1E-3 || obj_case2[1] - 1E-3 > obj_case1[1] {
1152                crate::verbosity_log!(
1153                    crate::diag::Verbosity::Summary,
1154                    "omega = {omega:?}\nphi = {phi:?}"
1155                );
1156                crate::verbosity_log!(
1157                    crate::diag::Verbosity::Summary,
1158                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1159                    obj_case1[0],
1160                    obj_case1[1]
1161                );
1162                crate::verbosity_log!(
1163                    crate::diag::Verbosity::Summary,
1164                    "Case 2: obj_time = {:.6}, obj_thermal_energy = {:.6}",
1165                    obj_case2[0],
1166                    obj_case2[1]
1167                );
1168                crate::verbosity_log!(
1169                    crate::diag::Verbosity::Summary,
1170                    "Interesting... Cases 1 and 2"
1171                );
1172            }
1173
1174            // Case 3: Time and TotalVariationTorque
1175            let obj_case3 = [
1176                CoppObjective::Time(1.0),
1177                CoppObjective::TotalVariationTorque(1.0, &vec![1.0; dim]),
1178            ];
1179            copp2_problem.objectives = &obj_case3;
1180            let start = Instant::now();
1181            let mut a_case3 = copp2_socp(&copp2_problem, &options_socp)?;
1182            let tc_copp2_case3 = start.elapsed().as_secs_f64() * 1E3;
1183            robot
1184                .constraints
1185                .project_to_feasible_topp2(&mut a_case3, &a_feasible, 0)?;
1186            let (_, obj_case3) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case3);
1187            if obj_case3[1] < obj_case1[1] - 1E-3 || obj_case3[2] - 1E-3 > obj_case1[2] {
1188                crate::verbosity_log!(
1189                    crate::diag::Verbosity::Summary,
1190                    "omega = {omega:?}\nphi = {phi:?}"
1191                );
1192                crate::verbosity_log!(
1193                    crate::diag::Verbosity::Summary,
1194                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1195                    obj_case1[0],
1196                    obj_case1[1],
1197                    obj_case1[2]
1198                );
1199                crate::verbosity_log!(
1200                    crate::diag::Verbosity::Summary,
1201                    "Case 3: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_total_variation_torque = {:.6}",
1202                    obj_case3[0],
1203                    obj_case3[1],
1204                    obj_case3[2]
1205                );
1206                crate::verbosity_log!(
1207                    crate::diag::Verbosity::Summary,
1208                    "Interesting... Cases 1 and 3"
1209                );
1210            }
1211
1212            // Case 4: Time and Linear
1213            let obj_case4 = [
1214                CoppObjective::Time(1.0),
1215                CoppObjective::Linear(1.0, &vec![0.0; n], &vec![1.0; n - 1]),
1216            ];
1217            copp2_problem.objectives = &obj_case4;
1218            let start = Instant::now();
1219            let mut a_case4 = copp2_socp(&copp2_problem, &options_socp)?;
1220            let tc_copp2_case4 = start.elapsed().as_secs_f64() * 1E3;
1221            robot
1222                .constraints
1223                .project_to_feasible_topp2(&mut a_case4, &a_feasible, 0)?;
1224            let (_, obj_case4) = objective_value_copp2_opt(&robot, 0, &objectives_test, &a_case4);
1225            if obj_case4[1] < obj_case1[1] - 1E-3 || obj_case4[3] - 1E-3 > obj_case1[3] {
1226                crate::verbosity_log!(
1227                    crate::diag::Verbosity::Summary,
1228                    "omega = {omega:?}\nphi = {phi:?}"
1229                );
1230                crate::verbosity_log!(
1231                    crate::diag::Verbosity::Summary,
1232                    "Case 1: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1233                    obj_case1[0],
1234                    obj_case1[1],
1235                    obj_case1[3]
1236                );
1237                crate::verbosity_log!(
1238                    crate::diag::Verbosity::Summary,
1239                    "Case 4: obj_time = {:.6}, obj_thermal_energy = {:.6}, obj_linear = {:.6}",
1240                    obj_case4[0],
1241                    obj_case4[1],
1242                    obj_case4[3]
1243                );
1244                crate::verbosity_log!(
1245                    crate::diag::Verbosity::Summary,
1246                    "Interesting... Cases 1 and 4"
1247                );
1248            }
1249            if obj_case4[2] < obj_case2[2] - 1E-3 || obj_case4[3] - 1E-3 > obj_case2[3] {
1250                crate::verbosity_log!(
1251                    crate::diag::Verbosity::Summary,
1252                    "omega = {omega:?}\nphi = {phi:?}"
1253                );
1254                crate::verbosity_log!(
1255                    crate::diag::Verbosity::Summary,
1256                    "Case 2: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1257                    obj_case2[0],
1258                    obj_case2[2],
1259                    obj_case2[3]
1260                );
1261                crate::verbosity_log!(
1262                    crate::diag::Verbosity::Summary,
1263                    "Case 4: obj_time = {:.6}, obj_total_variation_torque = {:.6}, obj_linear = {:.6}",
1264                    obj_case4[0],
1265                    obj_case4[2],
1266                    obj_case4[3]
1267                );
1268                crate::verbosity_log!(
1269                    crate::diag::Verbosity::Summary,
1270                    "Interesting... Cases 2 and 4"
1271                );
1272            }
1273
1274            if flag_print_step {
1275                crate::verbosity_log!(
1276                    crate::diag::Verbosity::Summary,
1277                    "Exp #{}:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1278                    i_exp + 1,
1279                    tc_copp2_case0,
1280                    obj_case0,
1281                    tc_copp2_case1,
1282                    obj_case1,
1283                    tc_copp2_case2,
1284                    obj_case2,
1285                    tc_copp2_case3,
1286                    obj_case3,
1287                    tc_copp2_case4,
1288                    obj_case4
1289                );
1290            }
1291
1292            tc_sum_case0 += tc_copp2_case0;
1293            tc_sum_case1 += tc_copp2_case1;
1294            tc_sum_case2 += tc_copp2_case2;
1295            tc_sum_case3 += tc_copp2_case3;
1296            tc_sum_case4 += tc_copp2_case4;
1297            for i in 0..obj_case0.len() {
1298                obj_sum_case0[i] += obj_case0[i];
1299                obj_sum_case1[i] += obj_case1[i];
1300                obj_sum_case2[i] += obj_case2[i];
1301                obj_sum_case3[i] += obj_case3[i];
1302                obj_sum_case4[i] += obj_case4[i];
1303            }
1304        }
1305
1306        for i in 0..4 {
1307            obj_sum_case0[i] /= n_exp as f64;
1308            obj_sum_case1[i] /= n_exp as f64;
1309            obj_sum_case2[i] /= n_exp as f64;
1310            obj_sum_case3[i] /= n_exp as f64;
1311            obj_sum_case4[i] /= n_exp as f64;
1312        }
1313
1314        crate::verbosity_log!(
1315            crate::diag::Verbosity::Summary,
1316            "Average {} experiments:\n Case 0: tc={:.3}ms, obj={:?}\n Case 1: tc={:.3}ms, obj={:?}\n Case 2: tc={:.3}ms, obj={:?}\n Case 3: tc={:.3}ms, obj={:?}\n Case 4: tc={:.3}ms, obj={:?}",
1317            n_exp,
1318            tc_sum_case0 / n_exp as f64,
1319            obj_sum_case0,
1320            tc_sum_case1 / n_exp as f64,
1321            obj_sum_case1,
1322            tc_sum_case2 / n_exp as f64,
1323            obj_sum_case2,
1324            tc_sum_case3 / n_exp as f64,
1325            obj_sum_case3,
1326            tc_sum_case4 / n_exp as f64,
1327            obj_sum_case4
1328        );
1329
1330        Ok(())
1331    }
1332
1333    fn run_test_copp2_socp_once(_options_socp: &ClarabelOptions) -> Result<(), CoppError> {
1334        run_test_copp2_socp_repeated(1, false)
1335    }
1336
1337    fn run_one_copp2_socp_only_time_case(
1338        options_ra: &ReachSet2Options,
1339        options_socp: &ClarabelOptions,
1340    ) -> Result<(f64, f64, f64, f64, f64, f64), CoppError> {
1341        let n: usize = 1000;
1342        let mut robot = Robot::with_capacity(Plannar2LinkEnd::new(1.0, 1.0, 1.0, 1.0), n);
1343        let dim = robot.dim();
1344
1345        let mut rng = rand::rng();
1346        let (s, path, omega, phi) =
1347            lissajous_path_for_test(dim, n, &mut rng).expect("random range is valid");
1348        robot
1349            .with_s(&s.as_view())?
1350            .with_q_from_path_2nd(&path, 0, n)?;
1351        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
1352
1353        let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
1354        let start = Instant::now();
1355        let a_ra = topp2_ra(&topp2_problem, options_ra)?;
1356        let tc_ra = start.elapsed().as_secs_f64() * 1E3;
1357        let (tf_ra, _) = s_to_t_topp2(s.as_slice(), &a_ra, 0.0)?;
1358
1359        let obj1 = [CoppObjective::Linear(
1360            1.0,
1361            &vec![-1.0; n],
1362            &vec![0.0; n - 1],
1363        )];
1364        let mut copp2_problem =
1365            Copp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0), &obj1).build()?;
1366        let start = Instant::now();
1367        let a_lp = copp2_socp(&copp2_problem, options_socp)?;
1368        let tc_lp = start.elapsed().as_secs_f64() * 1E3;
1369        let (tf_lp, _) = s_to_t_topp2(s.as_slice(), &a_lp, 0.0)?;
1370
1371        copp2_problem.objectives = &[CoppObjective::Time(1.0)];
1372        let start = Instant::now();
1373        let a_qp = copp2_socp(&copp2_problem, options_socp)?;
1374        let tc_qp = start.elapsed().as_secs_f64() * 1E3;
1375        let (tf_qp, _) = s_to_t_topp2(s.as_slice(), &a_qp, 0.0)?;
1376
1377        if (tf_lp - tf_ra).abs() > 1e-3 || (tf_qp - tf_ra).abs() > 1e-3 {
1378            crate::verbosity_log!(
1379                crate::diag::Verbosity::Summary,
1380                "omega = {omega:?}\nphi = {phi:?}"
1381            );
1382            panic!("COPP2 time optimality failed!");
1383        }
1384
1385        Ok((tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp))
1386    }
1387
1388    fn run_test_copp2_socp_only_time_repeated(
1389        n_exp: usize,
1390        flag_print_step: bool,
1391    ) -> Result<(), CoppError> {
1392        let mut tc_sum_ra = 0.0;
1393        let mut tc_sum_lp = 0.0;
1394        let mut tc_sum_qp = 0.0;
1395        let mut tf_sum_ra = 0.0;
1396        let mut tf_sum_lp = 0.0;
1397        let mut tf_sum_qp = 0.0;
1398
1399        let options_ra = ReachSet2OptionsBuilder::new()
1400            .lp_feas_tol(1E-9)
1401            .a_cmp_abs_tol(1E-9)
1402            .a_cmp_rel_tol(1E-9)
1403            .build()?;
1404        let options_socp = ClarabelOptionsBuilder::new()
1405            .allow_almost_solved(true)
1406            .build()?;
1407
1408        for i_exp in 0..n_exp {
1409            let (tc_ra, tc_lp, tc_qp, tf_ra, tf_lp, tf_qp) =
1410                run_one_copp2_socp_only_time_case(&options_ra, &options_socp)?;
1411
1412            if flag_print_step {
1413                crate::verbosity_log!(
1414                    crate::diag::Verbosity::Summary,
1415                    "Exp #{}: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1416                    i_exp + 1,
1417                    tc_ra,
1418                    tc_lp,
1419                    tc_qp,
1420                    tf_ra,
1421                    tf_lp,
1422                    tf_qp,
1423                );
1424            }
1425
1426            tc_sum_ra += tc_ra;
1427            tc_sum_lp += tc_lp;
1428            tc_sum_qp += tc_qp;
1429            tf_sum_ra += tf_ra;
1430            tf_sum_lp += tf_lp;
1431            tf_sum_qp += tf_qp;
1432        }
1433
1434        crate::verbosity_log!(
1435            crate::diag::Verbosity::Summary,
1436            "Average {} experiments: tc_ra = {:.4} ms, tc_lp = {:.4} ms, tc_qp = {:.4} ms, tf_ra = {:.6}, tf_lp = {:.6}, tf_qp = {:.6}",
1437            n_exp,
1438            tc_sum_ra / n_exp as f64,
1439            tc_sum_lp / n_exp as f64,
1440            tc_sum_qp / n_exp as f64,
1441            tf_sum_ra / n_exp as f64,
1442            tf_sum_lp / n_exp as f64,
1443            tf_sum_qp / n_exp as f64
1444        );
1445
1446        Ok(())
1447    }
1448}